package com.thaiopensource.relaxng.output.rnc;

import java.io.IOException;
import java.io.Writer;

public class StreamingPrettyprinter implements Prettyprinter
{
  private final String lineSep;
  private final Writer w;

  static private class Group
  {
    /**
     * Serial number of the segment containing the close of the group or -1 if
     * the segment is not yet closed.
     */
    int closeSegmentSerial = -1;
    boolean broken;
    final Group parent;

    Group (final Group parent)
    {
      this.parent = parent;
      broken = (parent == null);
    }

    void setBroken ()
    {
      if (!broken)
      {
        broken = true;
        parent.setBroken ();
      }
    }
  }

  static private class Segment
  {
    /**
     * Reference to the next segment.
     */
    Segment next;
    /**
     * Number of characters to discard at the end of the segment if we break
     * immediately after the segment
     */
    int preBreakDiscardCount = -1;
    /**
     * The current group at the end of the segment.
     */
    Group group = null;
    /**
     * The indent to be written after the newline at the end of this segment
     */
    int indent;
    final int serial;
    private static final int ALLOC_SPARE = 5;
    private static final int ALLOC_INIT = 10;
    /**
     * The characters in the segment including the characters from the
     * terminating soft newline
     */
    char [] chars = new char [ALLOC_INIT];
    /**
     * The number of characters in chars
     */
    int length = 0;

    Segment (final int serial)
    {
      this.serial = serial;
    }

    void append (final String str)
    {
      if (str.length () > chars.length - length)
      {
        int newSize = chars.length * 2;
        if (newSize - length < str.length ())
          newSize = chars.length + str.length () + ALLOC_SPARE;
        final char [] newChars = new char [newSize];
        System.arraycopy (chars, 0, newChars, 0, length);
        chars = newChars;
      }
      str.getChars (0, str.length (), chars, length);
      length += str.length ();
    }
  }

  private Segment head;
  private Segment tail;
  private int nextSegmentSerial = 0;
  private Group currentGroup = new Group (null);
  private int currentIndent = 0;
  private int [] indentStack = new int [10];
  private int indentLevel = 0;
  /**
   * The total width of the segments between head and tail inclusive.
   */
  private int totalWidth = 0;
  /**
   * Available width on this line.
   */
  private int availWidth;
  /**
   * The last possible breakpoint discovered on the line is immediately after
   * this segment.
   */
  private Segment lastPossibleBreak = null;

  /**
   * Maximum allowable line width (not including newline char).
   */
  private final int maxWidth;

  private Group noBreakGroup = null;

  public StreamingPrettyprinter (final int maxWidth, final String lineSep, final Writer w)
  {
    this.lineSep = lineSep;
    this.w = w;
    this.maxWidth = maxWidth;
    this.availWidth = maxWidth;
    head = makeSegment ();
    tail = head;
  }

  private Segment makeSegment ()
  {
    return new Segment (nextSegmentSerial++);
  }

  public void startGroup ()
  {
    currentGroup = new Group (currentGroup);
  }

  public void endGroup ()
  {
    if (noBreakGroup == currentGroup)
      noBreakGroup = null;
    currentGroup.closeSegmentSerial = tail.serial;
    currentGroup = currentGroup.parent;
  }

  public void startNest (final String indent)
  {
    if (indentLevel >= indentStack.length)
    {
      final int [] newStack = new int [indentStack.length * 2];
      System.arraycopy (indentStack, 0, newStack, 0, indentStack.length);
      indentStack = newStack;
    }
    indentStack[indentLevel++] = currentIndent;
    currentIndent += indent.length ();
  }

  public void endNest ()
  {
    currentIndent = indentStack[--indentLevel];
  }

  public void text (final String str)
  {
    tail.append (str);
    totalWidth += str.length ();
    tryFlush (false);
  }

  public void softNewline (final String noBreak)
  {
    if (head == tail || noBreakGroup == null)
      lastPossibleBreak = tail;
    tail.append (noBreak);
    tail.preBreakDiscardCount = noBreak.length ();
    tail.group = currentGroup;
    if (noBreakGroup == null)
      noBreakGroup = currentGroup;
    tail.indent = currentIndent;
    totalWidth += tail.preBreakDiscardCount;
    final Segment tem = makeSegment ();
    tail.next = tem;
    tail = tem;
    tryFlush (false);
  }

  public void hardNewline ()
  {
    if (head == tail || noBreakGroup == null)
      lastPossibleBreak = tail;
    tail.preBreakDiscardCount = 0;
    tail.group = currentGroup;
    if (noBreakGroup == null)
      noBreakGroup = currentGroup;
    tail.indent = currentIndent;
    final Segment tem = makeSegment ();
    tail.next = tem;
    tail = tem;
    tryFlush (true);
  }

  private boolean shouldKeepLooking (final boolean hard)
  {
    if (lastPossibleBreak == null)
      return true;
    if (hard)
      return false;
    if (totalWidth > availWidth)
      return false;
    if (lastPossibleBreak.group.broken)
      return false;
    return true;
  }

  private void tryFlush (final boolean hard)
  {
    for (;;)
    {
      if (shouldKeepLooking (hard))
        return;
      Segment s = head;
      head = lastPossibleBreak.next;
      lastPossibleBreak.next = null;
      lastPossibleBreak.length -= lastPossibleBreak.preBreakDiscardCount;
      for (; s != null; s = s.next)
        write (s.chars, 0, s.length);
      writeNewline (lastPossibleBreak.indent);
      availWidth = maxWidth - lastPossibleBreak.indent;
      lastPossibleBreak.group.setBroken ();
      update ();
    }
  }

  private void update ()
  {
    lastPossibleBreak = null;
    totalWidth = 0;
    Group nbg = null;
    for (Segment s = head; s != tail; s = s.next)
    {
      if (nbg != null && nbg.closeSegmentSerial != -1 && s.serial >= nbg.closeSegmentSerial)
        nbg = null;
      totalWidth += s.length;
      if (lastPossibleBreak == null || (!lastPossibleBreak.group.broken && nbg == null))
        lastPossibleBreak = s;
      if (nbg == null)
        nbg = s.group;
    }
    totalWidth += tail.length;
    if (nbg != null && nbg.closeSegmentSerial == -1)
      noBreakGroup = nbg;
    else
      noBreakGroup = null;
  }

  public void close ()
  {
    if (tail.length != 0)
    {
      // Don't want spaces after final newline
      currentIndent = 0;
      hardNewline ();
    }
    else
      if (head != tail)
      {
        // Avoid spaces after final newline
        for (Segment s = head;; s = s.next)
        {
          if (s.next == tail)
          {
            s.indent = 0;
            break;
          }
        }
        tryFlush (true);
      }
    try
    {
      w.close ();
    }
    catch (final IOException e)
    {
      throw new Prettyprinter.WrappedException (e);
    }
  }

  private void write (final char [] chars, final int off, final int len)
  {
    try
    {
      w.write (chars, off, len);
    }
    catch (final IOException e)
    {
      throw new Prettyprinter.WrappedException (e);
    }
  }

  private void writeNewline (final int indent)
  {
    try
    {
      w.write (lineSep);
      for (int i = 0; i < indent; i++)
        w.write (' ');
    }
    catch (final IOException e)
    {
      throw new Prettyprinter.WrappedException (e);
    }
  }
}
